package net.xdclass.service.impl;

import cn.hutool.core.date.DateUtil;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.model.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.component.StoreEngine;
import net.xdclass.config.MinioConfig;
import net.xdclass.controller.req.FileChunkInitTaskReq;
import net.xdclass.controller.req.FileChunkMergeReq;
import net.xdclass.controller.req.FileUploadReq;
import net.xdclass.dto.FileChunkDTO;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.exception.BizException;
import net.xdclass.mapper.FileChunkMapper;
import net.xdclass.mapper.StorageMapper;
import net.xdclass.model.FileChunkDO;
import net.xdclass.model.StorageDO;
import net.xdclass.service.AccountFileService;
import net.xdclass.service.FileChunkService;
import net.xdclass.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author 姜阿木
 * @date 2025/7/11 15:51
 */
@Service
@Slf4j
public class FileChunkServiceImpl implements FileChunkService {
    @Autowired
    private StoreEngine fileStoreEngine;

    @Autowired
    private FileChunkMapper fileChunkMapper;

    @Autowired
    private StorageMapper storageMapper;
    @Qualifier("minio-net.xdclass.config.MinioConfig")
    @Autowired
    private MinioConfig minioConfig;

    @Autowired
    private AccountFileService fileService;


    /**
     * 初始化分片上传任务
     * 1、检查存储空间是否足够
     * 2、根据文件名判断内容类型
     * 3、初始化分片上传，获取上传ID
     * 4、创建上传任务实体并设置相关属性
     * 5、将任务插入数据库，构建并返回任务信息DTO
     * @param req
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileChunkDTO initFileChunkTask(FileChunkInitTaskReq req) {
        // 1、检查存储空间是否足够
        StorageDO storageDO = storageMapper.selectOne(new QueryWrapper<StorageDO>()
                .eq("account_id", req.getAccountId()));
        if (storageDO.getUsedSize() + req.getTotalSize() > storageDO.getTotalSize()){
            throw new BizException(BizCodeEnum.FILE_STORAGE_NOT_ENOUGH);
        }

        String objectKey = CommonUtil.getFilePath(req.getFileName());
        // 2、根据文件名判断内容类型
        String contentType = MediaTypeFactory.getMediaType(objectKey).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        // 配置元数据
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(contentType);

        // 3、初始化分片上传，获取上传ID
        InitiateMultipartUploadResult uploadResult =
                fileStoreEngine.initMultipartUploadTask(minioConfig.getBucketName(), objectKey, metadata);
        String uploadId = uploadResult.getUploadId();
        // 组装任务信息DO并保存
        int chunkNum = (int) Math.ceil(req.getChunkSize() * 1.0 / req.getChunkSize());

        FileChunkDO task = new FileChunkDO();
        task.setBucketName(minioConfig.getBucketName())
                .setChunkNum(chunkNum)
                .setChunkSize(req.getChunkSize())
                .setTotalSize(req.getTotalSize())
                .setFileName(req.getFileName())
                .setIdentifier(req.getIdentifier())
                .setObjectKey(objectKey)
                .setUploadId(uploadId)
                .setAccountId(req.getAccountId());
        // 4、保存到数据库
        fileChunkMapper.insert(task);
        // 5、返回任务信息DTO
        return new FileChunkDTO(task).setFinished(false).setExistPartList(new ArrayList<>());
    }

    /**
     * 获取临时上传地址
     * @param accountId
     * @param identifier
     * @param partNumber
     * @return
     */
    @Override
    public String genPreSignUploadUrl(Long accountId, String identifier, int partNumber) {
        // 1、查询任务信息
        FileChunkDO task = fileChunkMapper.selectOne(new QueryWrapper<FileChunkDO>().lambda()
                .eq(FileChunkDO::getIdentifier, identifier)
                .eq(FileChunkDO::getAccountId, accountId));

        if (task == null){
            throw new BizException(BizCodeEnum.FILE_CHUNK_TASK_NOT_EXISTS);
        }
        // 2、配置预签名过期时间
        Date expireDate = DateUtil.offsetMillisecond(new Date(), minioConfig.getPreSignUrlExpireTime().intValue());
        // 3、生成预签名上传地址
        Map<String, Object> params= new HashMap<>();
        params.put("uploadId", task.getUploadId());
        params.put("partNumber", partNumber);

        URL preSignedUrl = fileStoreEngine.genePreSignedUrl(minioConfig.getBucketName(),
                task.getObjectKey(), HttpMethod.PUT, expireDate, params);
        log.info("生成预签名上传地址preSignedUrl={}", preSignedUrl);

        return preSignedUrl.toString();
    }

    /**
     * *  获取任务和分片列表，检查是否足够合并
     * *  检查存储空间和更新
     * *  合并分片
     * *  判断合并分片是否成功
     * *  存储文件和关联信息到数据库
     * *  根据唯一标识符删除相关分片信息
     * 合并分片
     * @param req
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void mergeFileChunk(FileChunkMergeReq req) {
        // 1、获取任务信息,检查是否足够合并
        FileChunkDO task = fileChunkMapper.selectOne(new QueryWrapper<FileChunkDO>().lambda()
                .eq(FileChunkDO::getIdentifier, req.getIdentifier())
                .eq(FileChunkDO::getAccountId, req.getAccountId()));
        if (task == null){
            throw new BizException(BizCodeEnum.FILE_CHUNK_TASK_NOT_EXISTS);
        }
        PartListing partListing = fileStoreEngine.listMultipart(task.getBucketName(), task.getObjectKey(), task.getUploadId());
        List<PartSummary> parts = partListing.getParts();
        if (parts.size() != task.getChunkNum()){
            // 上传的分片数量和任务数量不一致
            throw new BizException(BizCodeEnum.FILE_CHUNK_NOT_ENOUGH);
        }
        // 2、检查存储空间和更新
        StorageDO storageDO = storageMapper.selectOne(new QueryWrapper<StorageDO>()
                .eq("account_id", req.getAccountId()));
        long realFileTotalSize = parts.stream().map(PartSummary::getSize).mapToLong(Long::longValue).sum();
        if (storageDO.getUsedSize() + task.getTotalSize() > storageDO.getTotalSize()){
            throw new BizException(BizCodeEnum.FILE_STORAGE_NOT_ENOUGH);
        }
        storageDO.setUsedSize(storageDO.getUsedSize() + realFileTotalSize);
        storageMapper.updateById(storageDO);

        // 3、合并分片
        CompleteMultipartUploadResult result = fileStoreEngine.mergeChunks(
                task.getBucketName(),
                task.getObjectKey(),
                task.getUploadId(), parts.stream()
                        .map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag()))
                        .collect(Collectors.toList()));
        // 4、判断合并分片是否成功
        if (result.getETag() != null){
            FileUploadReq fileUploadReq = new FileUploadReq()
                    .setFileName(task.getFileName())
                    .setIdentifier(task.getIdentifier())
                    .setFileSize(task.getTotalSize())
                    .setAccountId(req.getAccountId())
                    .setParentId(realFileTotalSize)
                    .setFile(null);
            // 5、存储文件和关联信息到数据库
            fileService.saveFileAndAccountFile(fileUploadReq, task.getObjectKey());
            // 6、根据唯一标识符删除相关分片信息
            fileChunkMapper.deleteById(task.getId());

            log.info("合并分片成功，删除相关分片信息");

        }

    }

    /**
     * 分片上传进度查询
     * @param accountId
     * @param identifier
     * @return
     */
    @Override
    public FileChunkDTO listFileChunk(Long accountId, String identifier) {
        // 1、查询任务信息
        FileChunkDO task = fileChunkMapper.selectOne(new QueryWrapper<FileChunkDO>()
                .eq("identifier", identifier)
                .eq("account_id", accountId));
        if (task == null || !identifier.equals(task.getIdentifier())){
            throw new BizException(BizCodeEnum.FILE_CHUNK_TASK_NOT_EXISTS);
        }

        FileChunkDTO result = new FileChunkDTO(task);
        // 2、检查文件是否已经存在
        boolean exist = fileStoreEngine.doesObjectExist(task.getBucketName(), task.getObjectKey());
        if (!exist){
            // 文件不存在,表示分片未上传完成，返回已上传的分片
            PartListing partListing = fileStoreEngine.listMultipart(task.getBucketName(), task.getObjectKey(), task.getUploadId());
            List<PartSummary> parts = partListing.getParts();
            if (parts.size() == task.getChunkNum()){
                result.setFinished(true).setExistPartList(parts);
            }else {
                result.setFinished(false).setExistPartList(parts);
            }
        }
        return result;
    }
}
